iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
AI Engineering

30天拆Agent:從Repo看設計系列 第 9 篇

Day 9|Codex Agent 架構:OpenAI 怎麼把 Model + Tool 做成一套 Runtime?

  • 分享至 

  • xImage
  •  

前幾天看 HolmesGPT 時,我們已經看過一個典型 Agent loop:

Model
  ↓
Tool Call
  ↓
Tool Result
  ↓
Model

Codex 本質上也沒有脫離這個模式。

假設我在 Codex 裡輸入:

login API 的測試失敗了,幫我找出原因並修掉。

對使用者來說只是一句話,但 Codex 內部可能是:

User
  ↓
Turn
  ↓
Model #1
  ↓
exec pytest
  ↓
test failed
  ↓
Model #2
  ↓
read login.py
  ↓
Model #3
  ↓
edit file
  ↓
Model #4
  ↓
exec pytest
  ↓
tests passed
  ↓
Model #5
  ↓
Final response

所以一個 Turn 可能包含多次 Model sampling、多次 Tool execution,直到 Model 不再要求 Tool,這個 Turn 才結束。

OpenAI 怎麼把這個常見的 Model → Tool → Result → Model loop,工程化成一套可長時間運作、可接不同 Client、可動態重建 Tool 與 Runtime Context 的 Agent Runtime。


1. 設計特色一:用 Submission / Event 做成雙向事件流

這一層真正值得看的,不只是「Client 和 Core 分開」。

更準確地說,Codex 把 Client 與 Agent Runtime 的互動做成:

Client → Submission → Codex Core
Client ← Event      ← Codex Core

技術上,Submission 比較像「Client 要 Core 做什麼」的 command;Event 則是 Core 執行過程中持續送出的狀態事件。

直接走一個例子就很清楚。

假設使用者輸入:

幫我跑 pytest,看看哪個測試壞掉。

① Client 先送一個 Submission

在 Codex Core 裡,Submission 的結構很單純:

codex-rs/core/src/session/submission.rs

pub(crate) struct Submission {
    pub id: String,
    pub op: Op,
    ...
}

這次使用者輸入會進入 Op::TurnInput:

codex-rs/protocol/src/protocol.rs

Op::TurnInput {
    request: ...,
    mode: ...,
    reply: ...,
}

也就是 Client 並不是呼叫:

run pytest

而是告訴 Core:

這裡有一個新的 TurnInput,請開始處理。

② Session 收到 Submission,開始執行 Turn

Session 背後有:

codex-rs/core/src/session/handlers.rs

submission_loop(...)

它持續接收 Submission,再依 Op 分派。

這次收到 TurnInput 後,後面會建立正常的 Agent task,最後進入 run_turn()。

到這裡,Client 的工作其實已經結束。

接下來:

呼叫 Model
執行 Tool
跑 pytest
取得 stdout
繼續 Model sampling

都是 Codex Core 自己處理。


③ Core 執行時,不等全部完成才回傳,而是不斷送 Event

假設 Model 決定執行:

pytest

Client 可能依序收到:


TurnStarted

ExecCommandBegin
command = pytest

ExecCommandOutputDelta
FAILED test_login.py

ExecCommandEnd
exit_code = 1

AgentMessage
test_login.py 失敗,接下來檢查 login service。

...

TurnComplete

這些 Event type 都定義在:codex-rs/protocol/src/protocol.rs

例如:

EventMsg::TurnStarted
EventMsg::ExecCommandBegin
EventMsg::ExecCommandOutputDelta
EventMsg::ExecCommandEnd
EventMsg::AgentMessage
EventMsg::TurnComplete

官方 thread-manager-sample 也直接示範 Client 如何:

codex-rs/thread-manager-sample/src/main.rs

thread.next_event().await

持續讀取這些 Event。

因此 CLI 畫面之所以可以即時顯示:

Running pytest...
FAILED test_login.py
Checking login_service.py...

不是 CLI 自己理解 Agent 的執行流程。

它只是收到 Core 發出的 Event,再決定怎麼呈現。


④ 互動不是單向:Client 隨時可以再送新的 Submission

這也是事件式設計最實際的地方。

假設 pytest 跑到一半,使用者按下中止。

Client 不需要等原本的 Turn 回傳。

它可以再送:

Op::Interrupt

而 Op::Interrupt 的註解也直接指出,Core 會以:

EventMsg::TurnAborted

回應。

所以這不是傳統的:

request
  ↓
等待
  ↓
response

而是:

Client                     Codex Core

TurnInput  ───────────────►

          ◄─────────────── TurnStarted
          ◄─────────────── ExecCommandBegin
          ◄─────────────── ExecCommandOutputDelta

Interrupt ────────────────►

          ◄─────────────── TurnAborted

這才是這一節真正的重點。

Codex 把 Agent 與 Client 的互動設計成持續的雙向事件流,而不是一次 request / response。

因此 Client 可以在 Agent 長時間工作時:

  • 即時顯示 Tool 執行狀態;
  • streaming 顯示輸出;
  • 中途 Interrupt;
  • 回覆需要使用者介入的互動;
  • 不需要理解 Agent loop 本身。

這種 event-driven 架構不是 Codex 獨有,但在 Codex 裡它是很明確的 Core boundary,也正是後面 App Server 能把同一套 Agent Runtime 接給其他 Client 的基礎。


2. 設計特色二:每次要問 Model 下一步前,Codex 都重新確認「現在的 Runtime」

StepContext 這個名字很抽象。

先看一個例子。

假設 Codex 正在做:

幫我把 login test 修好。

Model 第一次決定:

先跑 pytest。

Codex 執行完,得到:

FAILED test_login.py

接下來要再問 Model:

下一步要做什麼?

這時 Codex 不會只沿用 Turn 一開始那份固定設定,而是重新確認:

現在用哪個 Model?
現在在哪個 Environment?
現在 MCP 有哪些連線?
現在 Model 看得到哪些 Tool?
現在 AGENTS.md 有哪些規則?

確認完,才送出下一次 Model request。

這一份「這次 Model call 要使用的 Runtime 狀態」,Codex 裡叫:StepContext

位置:codex-rs/core/src/session/step_context.rs

可以把它想成:

第一次問 Model
→ 拍一張現在 Runtime 的照片
→ Model 決定跑 pytest

pytest 執行完

第二次問 Model
→ 再拍一張現在 Runtime 的照片
→ Model 決定讀 login.py

這樣做的原因很簡單:Agent 一個任務可能跑幾十秒甚至幾分鐘,中間環境可能改變。

例如:

MCP 重新連線
某個 Tool 被啟用
Environment 改變
設定被更新

如果後面的 Model call 還拿著舊狀態,就可能發生:

Model 以為某個 Tool 還能用,
但 Runtime 實際上已經不同。

所以 Codex 把 Runtime snapshot 的邊界做到每一次 Model request,而不只是 Turn。


ToolRouter 是這份 Runtime Snapshot 裡的一部分

StepContext 裡有一個:

codex-rs/core/src/tools/router.rs

tool_router: Arc<ToolRouter>

它負責回答兩件事:

這次 Model 看得到哪些 Tool?
這些 Tool 真正要交給哪個 Runtime 執行?

例如這次 Model 可以看到:

exec_command
read_file
write_file

Model 回:

exec_command("pytest")

Model 本身不會真的去執行 shell。

Codex 會把這個 Tool Call 轉成自己的 ToolCall,再交給對應 runtime。

簡單來說:

Model:
我要用 exec_command("pytest")

Codex Runtime:
找到 exec_command 的實作
→ 真正執行 pytest
→ 把結果送回 Model

ToolRouter 裡最重要的兩塊是:

model_visible_specs
→ 這次 Model 看得到哪些 Tool

ToolRegistry
→ 這些 Tool 真正由哪個 runtime 執行

相關 code:
codex-rs/core/src/tools/router.rs
codex-rs/core/src/tools/spec_plan.rs

其中:

build_tool_router(...)

會根據當下的:

Model
Environment
MCP
Tool Policy
Dynamic Tools

整理出這一次 sampling 使用的 Tool plan。

也就是:

準備下一次 Model call
        ↓
重新確認 Runtime
        ↓
建立 StepContext
        ↓
整理這次可見的 Tools
        ↓
Model 做下一步判斷

Tool plan 被放進每一次 Model request 的 StepContext 裡,會跟著當下的 Environment、MCP 與設定一起重新確認。


3. 一個例子,把兩個特色串起來

回到最開始:

login API 的 test 掛了,幫我修掉。

整段可以簡化成:

Client
  │
  │ Submission: TurnInput
  ▼
Codex Core
  │
  ├── Event: TurnStarted
  │
  ▼
準備問 Model 下一步
  │
  ├── 重新確認 Environment
  ├── 重新確認 MCP
  ├── 重新確認 Tools
  └── 建立 StepContext
  │
  ▼
Model
  │
  │ exec_command("pytest")
  ▼
Codex Runtime 執行 Tool
  │
  ├── Event: ExecCommandBegin
  ├── Event: ExecCommandOutputDelta
  └── Event: ExecCommandEnd
  │
  ▼
再準備下一次 Model call
  │
  └── 重新建立當下 StepContext
  │
  ▼
Model 繼續下一步
  │
  ...
  ▼
Final Response
  │
  └── Event: TurnComplete
  ▼
Client

這張圖其實就把今天兩個重點放在一起:

  1. Client 與 Core 不是單次 request / response,而是持續交換 Submission / Event。
  2. Core 每次要讓 Model 做下一個判斷前,都重新確認當下 Runtime。

結論:Codex 的特色不是發明新的 Agent Loop,而是把 Loop 工程化

如果只看核心循環:

Model
 ↓
Tool
 ↓
Observation
 ↓
Model

Codex 沒有什麼特別。

HolmesGPT 也有。

ReAct 本來就在做。

Codex 真正值得研究的是:

它怎麼把一個普通 Agent loop 拆成清楚的 runtime boundary。

最後可以濃縮成:

Client
  │
  │ Submission
  ▼
Codex Core
  │
  ├── 每次 Model call 前重新確認 Runtime
  │      ├── Environment
  │      ├── MCP
  │      └── Tools
  │
  ▼
Model ↔ Tool Runtime
  │
  │ Event
  ▼
Client

這才是我認為 Codex 第一篇最值得留下來的結論:

Codex 的 Agent loop 本身並不特別;真正值得看的,是它把 Client/Core 互動做成雙向 protocol,並在每次 Model request 前重新整理當下 Runtime。


References


上一篇
Day 8|HolmesGPT 的 Memory 有什麼特別?從 Checkpoint、Resume 到 Feedback
下一篇
Day 10|Codex App Server:把 Codex Core 接到真正的產品介面
系列文
30天拆Agent:從Repo看設計 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言